package com.jiyongjun.component.core.model;

import com.jiyongjun.component.config.LogBootstrapConfig;
import com.jiyongjun.component.core.annotation.JsonObject;
import com.jiyongjun.component.enums.LogApiTypeEnum;
import com.jiyongjun.component.enums.LogConstant;
import com.jiyongjun.component.enums.LogResultTypeEnum;
import com.jiyongjun.component.exception.BizException;
import com.jiyongjun.component.exception.ServiceException;
import com.jiyongjun.component.sls.SlsLogService;
import com.jiyongjun.component.utils.JsonUtils;
import com.jiyongjun.component.utils.SpringContextHolder;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.experimental.FieldNameConstants;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.constraints.NotNull;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

import static com.jiyongjun.component.enums.LogConstant.LOG_EXCEPTION_KEYWORD;

/**
 * <h2></h2>
 * <h3>描述：</h3>
 *
 * @author yongjun.jyj
 * @since 2022/7/14 17:42
 */
@Setter
@Getter
@Accessors(chain = true)
@FieldNameConstants
public abstract class BaseLogContext <T extends BaseLogContext<T>>{
    // ************ 接口元信息 ************
    /** 接口唯一名称：fullClassName#methodName(param0Type,...paramNType) */
    private final String apiUniqueName;
    /**
     * 接口显示名称
     * <p>
     * <li>MTOP默认为MTOP接口名</li>
     * <li>TOP默认为TOP接口名</li>
     * <li>其他为className#methodName</li>
     * </p>
     */
    private String apiName;
    /** 接口描述 */
    protected String apiDesc;
    /** 接口类型 {@link LogApiTypeEnum#getCode()} */
    protected String apiType;

    /** 跟apiUniqueName有重复，先不序列化到日志，后续有需要再打开 */
    @JsonObject
    private transient InvokerContext invokerContext = new InvokerContext();

    // ************ 接口调用信息 ************
    /** 所属环境：daily/pre/online */
    @Setter(AccessLevel.NONE)
    private String env;
    /** traceId */
    private final String traceId;
    /** 入参 */
    @JsonObject
    protected String args;
    /** 出参 */
    @JsonObject
    private String result;
    /** 出参类型 {@link LogResultTypeEnum#getCode()} */
    private String resultType = LogResultTypeEnum.SUCCESS.getCode();
    @Setter(AccessLevel.NONE)
    private Boolean success;
    @Setter(AccessLevel.NONE)
    protected String errorCode;
    @Setter(AccessLevel.NONE)
    protected String errorMsg;
    @JsonObject
    private String errorPayload;
    /** 异常堆栈 */
    @Setter(AccessLevel.NONE)
    protected String stackTrace;

    @Setter(AccessLevel.NONE)
    private Date time;

    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    private transient Long start;
    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    private transient Long end;
    @Setter(AccessLevel.NONE)
    protected Long elapsedTime;

    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    private transient boolean closed;

    // ************ 用户信息 ************
    private Long userId;
    private String userNick;

    /** 扩展信息 */
    private final Map<String, Object> extInfoMap = new HashMap<>();
    private transient final ThreadLocal<Map<String, BaseLogContext<?>>> logThreadLocal;

    private transient static final Logger LOGGER = LoggerFactory.getLogger(BaseLogContext.class);

    /**
     *
     * @param apiUniqueName 接口名
     * @param logThreadLocal 日志上下文
     */
    protected BaseLogContext(String apiUniqueName, ThreadLocal<Map<String, BaseLogContext<?>>> logThreadLocal) {
        if (apiUniqueName == null) {
            // 用于构造无操作对象
            this.apiUniqueName = null;
            this.traceId = null;
            this.logThreadLocal = null;
            return;
        }
        this.apiUniqueName = apiUniqueName;
        this.logThreadLocal = logThreadLocal;
        // todo traceId需要适配
        this.traceId = UUID.randomUUID().toString();
        this.start = System.currentTimeMillis();
        this.time = new Date();
        this.env = SpringContextHolder.getBeanWithCache(LogBootstrapConfig.class).getEnv();
        // todo 用户信息需要记录
        this.userId = null;
        this.userNick = null;

    }

    public T addExtInfo(String key, Object value) {
        extInfoMap.put(key, value);
        return (T) this;
    }

    public T setApiName(String apiName) {
        this.apiName = apiName;
        return (T) this;
    }

    public T setBizException(BizException exception) {
        setBizException(exception.getMsgCode(), exception.getMessage(), exception);
        this.errorPayload = JsonUtils.toJson(exception.getPayload());
        return (T) this;
    }

    public T setBizException(String errorCode, @NotNull String errorMsg, Throwable throwable) {
        this.errorCode = Optional.ofNullable(errorCode).orElse(LogConstant.DEFAULT_BIZ_EXCEPTION_CODE);
        this.errorMsg = errorMsg;
        this.resultType = LogResultTypeEnum.BIZ_EXCEPTION.getCode();
        if (throwable != null) {
            this.stackTrace = getStackTraceMessage(throwable, 6);
        }
        return (T) this;
    }

    public T setServiceException(ServiceException e) {
        setServiceException(e.getMsgCode(), e.getMessage(), e);
        this.errorPayload = JsonUtils.toJson(e.getPayload());
        return (T) this;
    }

    public T setServiceException(String errorCode, String errorMsg, Throwable throwable) {
        this.errorCode = Optional.ofNullable(errorCode).orElse(LogConstant.DEFAULT_SERVICE_EXCEPTION_CODE);
        this.errorMsg = errorMsg;
        this.resultType = LogResultTypeEnum.SERVICE_EXCEPTION.getCode();
        if (throwable != null) {
            this.stackTrace = ExceptionUtils.getStackTrace(throwable);
        }
        return (T) this;
    }

    public T setException(Throwable throwable) {
        return setServiceException(null, throwable.getMessage(), throwable);
    }

    protected static String getStackTraceMessage(Throwable e, int maxLine) {
        return e + "\r\n\tat " + Arrays.stream(e.getStackTrace()).limit(maxLine)
                .map(StackTraceElement::toString)
                .collect(Collectors.joining("\r\n\tat "));
    }

    protected T setEnd(Long end) {
        this.end = end;
        if (this.start != null && this.end != null) {
            this.elapsedTime = this.end - this.start;
        }
        return (T) this;
    }

    public T setArgs(Object obj) {
        this.args = JsonUtils.toJson(obj);
        return (T) this;
    }

    public T setMethod(Method method) {
        this.invokerContext.setClassName(method.getDeclaringClass().getName())
                .setMethodName(method.getName());
        return (T) this;
    }

    public T setResult(Object obj) {
        this.result = JsonUtils.toJson(obj);
        return (T) this;
    }

    public Boolean getSuccess() {
        return LogResultTypeEnum.SUCCESS.getCode().equals(resultType);
    }

    public void close() {
        // 无用日志对象不做处理
        if (apiUniqueName == null) {
            return;
        }
        if (closed) {
            return;
        }
        closed = true;
        if (this.end == null) {
            this.setEnd(System.currentTimeMillis());
        }
        try {
            Map<String, BaseLogContext<?>> logVOMap = this.logThreadLocal.get();
            logVOMap.remove(apiUniqueName);
            if (logVOMap.isEmpty()) {
                this.logThreadLocal.remove();
            }
            print();
        } catch (Throwable throwable) {
            LOGGER.warn("{}|输出日志异常|exception|msg={}", LOG_EXCEPTION_KEYWORD, throwable.getMessage(), throwable);
        }
    }

    protected void print() {
        SlsLogService.getInstance().log(this);
    }

    @Data
    @Accessors(chain = true)
    private static class InvokerContext {
        private String className;
        private String methodName;
    }
}
